unit RaResizer;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls;

const
   GRIDDEFAULT = 4;

type
   TRaResizer = class;
   TMover = class;

   TMovingEvent = procedure(Sender: TRaResizer; var NewLeft, NewTop: integer) of object;
   TSizingEvent = procedure(Sender: TRaResizer; var NewLeft, NewTop, NewWidth, NewHeight: integer) of object;

   TRaResizer = class(TComponent)
   protected
      FActive     : boolean;
      FControl    : TControl;
      Sizers      : TList;
      GroupMovers : TList;
      FGroup      : TWinControl;
      FGridX      : integer;
      FGridY      : integer;
      FOnSized    : TNotifyEvent;
      FOnSizing   : TSizingEvent;
      FOnMoved    : TNotifyEvent;
      FOnMoving   : TMovingEvent;
      Sizing      : boolean;
      Moving      : boolean;
      OrigSize    : TRect;
      NewSize     : TRect;
      DownX       : integer;
      DownY       : integer;
      FAllowSize  : boolean;
      FAllowMove  : boolean;
      FKeepIn     : boolean;
      FHotTrack   : boolean;
      OneMover    : TMover;
      CurMover    : TMover;
      procedure   Notification(AComponent: TComponent; Operation: TOperation); override;
      procedure   SetActive(b: boolean);
      procedure   SetControl(c: TControl);
      procedure   SetGroup(p: TWinControl);
      procedure   CreateSizers;
      procedure   CheckSizers;
      procedure   ShowSizers;
      procedure   HideSizers;
      procedure   SizerDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
      procedure   SizerUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
      procedure   SizerMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
      procedure   MoverDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
      procedure   MoverUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
      procedure   MoverMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
      procedure   DrawSizeRect(Rect: TRect);
      procedure   Calc_Size_Rect(SizerNum, dx, dy: integer);
      procedure   DoSizingEvent;
      procedure   Calc_Move_Rect(dx, dy: integer);
      procedure   DoMovingEvent;
      procedure   Constrain_Size;
      procedure   Constrain_Move;
      procedure   MoverKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
      procedure   DoSizeMove(var Key: Word; Shift: TShiftState; dx, dy: integer);
      procedure   CreateGroupMovers;
      procedure   CreateOneMover(m: TMover; c: TControl);
      function    FindMoverByBuddy(c: TControl): TMover;
   public
      constructor Create(AOwner: TComponent); override;
      destructor  Destroy; override;
   published
      property    Active: boolean read FActive write SetActive default True;
      property    ResizeControl: TControl read FControl write SetControl;
      property    ResizeGroup: TWinControl read FGroup write SetGroup;
      property    GridX: integer read FGridX write FGridX default GRIDDEFAULT;
      property    GridY: integer read FGridY write FGridY default GRIDDEFAULT;
      property    OnSized: TNotifyEvent read FOnSized write FOnSized;
      property    OnSizing: TSizingEvent read FOnSizing write FOnSizing;
      property    OnMoved: TNotifyEvent read FOnMoved write FOnMoved;
      property    OnMoving: TMovingEvent read FOnMoving write FOnMoving;
      property    AllowSize: boolean read FAllowSize write FAllowSize default True;
      property    AllowMove: boolean read FAllowMove write FAllowMove default True;
      property    KeepInParent: boolean read FKeepIn write FKeepIn default True;
      property    HotTrack: boolean read FHotTrack write FHotTrack;
   end;

   TInvisWin = class(TPanel)     // This could also derive from TPanel
   protected
      procedure WndProc(var Message: TMessage); override;
      procedure CreateParams(var Params: TCreateParams); override;
      procedure WMDLGCode(var Message: TMessage); message WM_GETDLGCODE;
   public
      property  OnKeyDown;
   end;

   TMover = class(TInvisWin)
   public
      Buddy     : TControl;
      procedure Show;
   end;


procedure Register;

implementation

const
   SIZE     = 6;
   HALFSIZE = SIZE div 2;

type
   TSizer = class(TPanel)
   end;

procedure Register;
begin
  RegisterComponents('Rarnu Components', [TRaResizer]);
end;


// *****************************************************************

procedure TInvisWin.WndProc(var Message: TMessage);
var
   ps : TPaintStruct;
begin
   case Message.Msg of
      WM_ERASEBKGND: Message.Result := 1;
      WM_PAINT: begin
         BeginPaint(Handle, ps);
         EndPaint(Handle, ps);            
         Message.Result := 1;
      end;
   else
      inherited WndProc(Message);
   end;
end;

procedure TInvisWin.CreateParams(var Params: TCreateParams);
begin
   inherited;
   Params.ExStyle := Params.ExStyle or WS_EX_TRANSPARENT;
end;

procedure TInvisWin.WMDLGCode(var Message: TMessage);
begin
   Message.Result := DLGC_WANTARROWS or DLGC_WANTALLKEYS;
end;


// *****************************************************************
// TMover

procedure TMover.Show;
begin
   Assert(Buddy <> nil);
   BoundsRect := Buddy.BoundsRect;
   Parent     := Buddy.Parent;
   Visible    := True;
   BringToFront;
end;


// *****************************************************************
// TRaResizer

constructor TRaResizer.Create(AOwner: TComponent);
begin
   inherited;
   FActive      := True;
   FKeepIn      := True;
   FGridX       := GRIDDEFAULT;
   FGridY       := GRIDDEFAULT;
   FAllowSize   := True;
   FAllowMove   := True;
   GroupMovers  := TList.Create;
   Sizers       := TList.Create;

   OneMover := TMover.Create(Self);
   CreateOneMover(OneMover, nil);

   CreateSizers;
end;

destructor TRaResizer.Destroy;
begin
   GroupMovers.Free;
   Sizers.Free;
   Sizers := nil;
   inherited;
end;

procedure TRaResizer.Notification(AComponent: TComponent; Operation: TOperation);
begin
   inherited;
   if csDestroying in ComponentState then exit;
   if (AComponent = ResizeControl) and (Operation = opRemove) then
      ResizeControl := nil;
end;

procedure TRaResizer.SetActive(b: boolean);
begin
   if b<>FActive then begin
      FActive := b;
      CheckSizers;
   end;
end;

procedure TRaResizer.SetControl(c: TControl);
begin
   if c <> FControl then begin

      if c<>nil then begin
         if ResizeGroup<>nil then begin
            Assert(c.Parent = ResizeGroup, 'ResizeControl is not in ResizeGroup!');
            CurMover := FindMoverByBuddy(c);
         end else begin
            CurMover := OneMover;
            CurMover.Buddy := c;
         end;
         CurMover.Show;
      end;

      FControl := c;
      CheckSizers;
   end;
end;

procedure TRaResizer.SetGroup(p: TWinControl);
begin
   if p <> FGroup then begin
      FGroup := p;
      CreateGroupMovers;
   end;
end;

procedure TRaResizer.CreateGroupMovers;
var
   i : integer;
   m : TMover;
   c : TControl;
begin
   if csDesigning in ComponentState then exit;

   // Clear out the old Movers
   for i := 0 to GroupMovers.Count-1 do
      TObject(GroupMovers[i]).Free;
   GroupMovers.Clear;

   if ResizeGroup <> nil then begin
      for i := 0 to ResizeGroup.ControlCount-1 do begin
         c := ResizeGroup.Controls[i];
         if (c is TMover) or (c is TSizer) then continue;

         m := TMover.Create(Self);
         CreateOneMover(m, c);
         GroupMovers.Add(m);
         m.Show;
      end;
   end;
end;

procedure TRaResizer.CreateSizers;
var
   i : integer;
   p : TSizer;
begin
   if csDesigning in ComponentState then exit;

   for i := 0 to 7 do begin
      p := TSizer.Create(Self);
      Sizers.Add(p);

      p.BevelOuter   := bvNone;
      p.Width        := SIZE;
      p.Height       := SIZE;
      p.Color        := clBlack;
      p.Caption      := '';
      p.Tag          := i;
      p.OnMouseDown  := SizerDown;
      p.OnMouseUp    := SizerUp;
      p.OnMouseMove  := SizerMove;
      p.TabStop      := False;

      case i of
         0, 7  : p.Cursor := crSizeNWSE;
         2, 5  : p.Cursor := crSizeNESW;
         1, 6  : p.Cursor := crSizeNS;
         3, 4  : p.Cursor := crSizeWE;
      end;
   end;
end;

procedure TRaResizer.CreateOneMover(m: TMover; c: TControl);
begin
   m.OnMouseDown := MoverDown;
   m.OnMouseUp   := MoverUp;
   m.OnMouseMove := MoverMove;
   m.TabStop     := True;
   m.OnKeyDown   := MoverKeyDown;
   m.Buddy       := c;
end;

procedure TRaResizer.CheckSizers;
begin
   if (ResizeControl<>nil) and Active and (not (csDesigning in ComponentState)) then
      ShowSizers
   else
      HideSizers;
end;

procedure TRaResizer.ShowSizers;
var
   i : integer;
   p : TPanel;
   c : TControl;
begin
   c := ResizeControl;
   Assert(c <> nil);

   for i := 0 to 7 do begin
      p := TPanel(Sizers[i]);
      case i of
         0, 1, 2 : p.Top := c.Top - HALFSIZE;
         3,    4 : p.Top := c.Top + c.Height div 2 - HALFSIZE;
         5, 6, 7 : p.Top := c.Top + c.Height - HALFSIZE;
      end;

      case i of
         0, 3, 5 : p.Left := c.Left - HALFSIZE;
         1,    6 : p.Left := c.Left + c.Width div 2 - HALFSIZE;
         2, 4, 7 : p.Left := c.Left + c.Width - HALFSIZE;
      end;
   end;

   Assert(CurMover<>nil);
   CurMover.Show;

   for i := 0 to Sizers.Count-1 do begin
      p := TPanel(Sizers[i]);
      p.Parent  := c.Parent;
      p.Visible := True;
      p.BringToFront;
   end;

   if CurMover.HandleAllocated and CurMover.CanFocus then
      CurMover.SetFocus;
end;

procedure TRaResizer.HideSizers;
var
   i : integer;
   p : TPanel;
begin
   for i := 0 to Sizers.Count-1 do begin
      p := TPanel(Sizers[i]);
      p.Visible := False;
      p.Update;
   end;
   OneMover.Visible := False;
end;

procedure TRaResizer.SizerDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
   Sizing  := True;
   DownX   := X;
   DownY   := Y;
   HideSizers;
   ResizeControl.Parent.Update;
   ResizeControl.Update;
   OrigSize := ResizeControl.BoundsRect;
   NewSize  := OrigSize;
   DrawSizeRect(NewSize);
end;

procedure DoSwap(DoSwap: boolean; var a, b: integer);
var
   t : integer;
begin
   if DoSwap then begin
      t := a;
      a := b;
      b := t;
   end;
end;

procedure TRaResizer.SizerUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
   if NewSize.Right < NewSize.Left then
      DoSwap(True, NewSize.Right, NewSize.Left);
   if NewSize.Bottom < NewSize.Top then
      DoSwap(True, NewSize.Bottom, NewSize.Top);
      
   Sizing := False;
   DrawSizeRect(NewSize);
   ResizeControl.Invalidate;
   ResizeControl.BoundsRect := NewSize;
   ShowSizers;
   if Assigned(OnSized) then OnSized(Self);
end;

procedure TRaResizer.SizerMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
   if Sizing then begin
      DrawSizeRect(NewSize);

      if AllowSize then begin
         Calc_Size_Rect((Sender as TSizer).Tag, X - DownX, Y - DownY);
         DoSizingEvent;
      end;

      DrawSizeRect(NewSize);
      if HotTrack then ResizeControl.BoundsRect := NewSize;
   end;
end;

procedure TRaResizer.DoSizingEvent;
var
   tmpWid, tmpHgt  : integer;
begin
   tmpWid := NewSize.Right - NewSize.Left;
   tmpHgt := NewSize.Bottom - NewSize.Top;
   if Assigned(OnSizing) then
      OnSizing(Self, NewSize.Left, NewSize.Top, tmpWid, tmpHgt);
   NewSize.Right  := NewSize.Left + tmpWid;
   NewSize.Bottom := NewSize.Top + tmpHgt;
end;

procedure GetNonClientOffset(h: THandle; var nx, ny: integer);
var
   p : TPoint;
   R : TRect;
begin
   p := Point(0, 0);
   Windows.ClientToScreen(h, p);
   Windows.GetWindowRect(h, R);
   nx := p.x - R.Left;
   ny := p.y - R.Top;
end;

procedure TRaResizer.DrawSizeRect(Rect: TRect);
var
   h        : THandle;
   dc       : THandle;
   c        : TCanvas;
   nx, ny   : integer;
   OldPen   : TPen;
   OldBrush : TBrush;
begin
   if HotTrack then exit;

   h  := (ResizeControl.Parent as TWinControl).Handle;
   GetNonClientOffset(h, nx, ny);
   dc := GetWindowDC(h);
   try
      c := TCanvas.Create;
      c.Handle := dc;

      OldPen := TPen.Create;
      OldPen.Assign(c.Pen);
      OldBrush := TBrush.Create;
      OldBrush.Assign(c.Brush);

      c.Pen.Width := 2;
      c.Pen.Mode  := pmXOR;
      c.Pen.Color := clWhite;
      c.Brush.Style := bsClear;
      c.Rectangle(Rect.Left + nx, Rect.Top + ny, Rect.Right + nx, Rect.Bottom + ny);

      c.Pen.Assign(OldPen);
      OldPen.Free;
      c.Brush.Assign(OldBrush);
      OldBrush.Free;

      c.Handle := 0;
      c.Free;
   finally
      ReleaseDC(h, dc);
   end;
end;

procedure TRaResizer.Calc_Size_Rect(SizerNum, dx, dy: integer);
begin
   dx := (dx div GridX) * GridX;
   dy := (dy div GridY) * GridY;

   case SizerNum of
      0, 1, 2 : NewSize.Top    := OrigSize.Top + dy;
      5, 6, 7 : NewSize.Bottom := OrigSize.Bottom + dy;
   end;

   case SizerNum of
      0, 3, 5 : NewSize.Left   := OrigSize.Left + dx;
      2, 4, 7 : NewSize.Right  := OrigSize.Right + dx;
   end;

   if KeepInParent then Constrain_Size;
end;

procedure TRaResizer.MoverDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
   CurMover := Sender as TMover;
   FControl := CurMover.Buddy;
   Assert(FControl<>nil);
   FControl.BringToFront;
   CurMover.BringToFront;

   Moving := True;
   DownX := X;
   DownY := Y;
   HideSizers;
   ResizeControl.Parent.Update;
   ResizeControl.Update;
   OrigSize := ResizeControl.BoundsRect;
   NewSize  := OrigSize;
   DrawSizeRect(NewSize);
end;

procedure TRaResizer.MoverUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
   Moving := False;
   ResizeControl.BoundsRect := NewSize;
   CurMover.Invalidate;
   ResizeControl.Refresh;
   DrawSizeRect(NewSize);
   ShowSizers;
   if Assigned(OnMoved) then OnMoved(Self);
end;

procedure TRaResizer.Calc_Move_Rect(dx, dy: integer);
begin
   NewSize := OrigSize;
   dx := (dx div GridX) * GridX;
   dy := (dy div GridY) * GridY;
   OffsetRect(NewSize, dx, dy);
   if KeepInParent then Constrain_Move;
end;

procedure TRaResizer.DoMovingEvent;
var
   tmpWid, tmpHgt : integer;
begin
   tmpWid := NewSize.Right - NewSize.Left;
   tmpHgt := NewSize.Bottom - NewSize.Top;
   if Assigned(OnMoving) then
      OnMoving(Self, NewSize.Left, NewSize.Top);
   NewSize.Right := NewSize.Left + tmpWid;
   NewSize.Bottom := NewSize.Top + tmpHgt;
end;

procedure TRaResizer.MoverMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
   dx, dy: integer;
begin
   if Moving then begin
      DrawSizeRect(NewSize);

      if AllowMove then begin
         dx := X - DownX;
         dy := Y - DownY;
         Calc_Move_Rect(dx, dy);
         DoMovingEvent;
      end;

      DrawSizeRect(NewSize);
      if HotTrack then ResizeControl.BoundsRect := NewSize;
   end;
end;

procedure TRaResizer.Constrain_Size;
var
   p : TWinControl;
begin
   p := ResizeControl.Parent;

   with NewSize do begin
      if Left < 0 then Left := 0;
      if Top < 0 then Top := 0;
      if Right > p.ClientWidth then Right := p.ClientWidth;
      if Bottom > p.ClientHeight then Bottom := p.ClientHeight;

      if Right < Left + GridX then Right := Left + GridX;
      if Bottom < Top + GridY then Bottom := Top + GridY;
   end;
end;

procedure TRaResizer.Constrain_Move;
begin
   if NewSize.Left < 0 then
      OffsetRect(NewSize, -NewSize.Left, 0);

   if NewSize.Top < 0 then
      OffsetRect(NewSize, 0, -NewSize.Top);

   if NewSize.Right > ResizeControl.Parent.ClientWidth then
      OffsetRect(NewSize, ResizeControl.Parent.ClientWidth - NewSize.Right, 0);

   if NewSize.Bottom > ResizeControl.Parent.ClientHeight then
      OffsetRect(NewSize, 0, ResizeControl.Parent.ClientHeight - NewSize.Bottom);
end;

procedure TRaResizer.MoverKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
   if Active then begin
      case Key of
         VK_LEFT  : DoSizeMove(Key, Shift, -GridX,  0);
         VK_RIGHT : DoSizeMove(Key, Shift,  GridX,  0);
         VK_UP    : DoSizeMove(Key, Shift,  0, -GridY);
         VK_DOWN  : DoSizeMove(Key, Shift,  0,  GridY);
      end;
   end;
end;

procedure TRaResizer.DoSizeMove(var Key: Word; Shift: TShiftState; dx, dy: integer);
begin
   if (ssCtrl in Shift) or (ssShift in Shift) then begin
      Key := 0;

      NewSize := ResizeControl.BoundsRect;

      if (ssCtrl in Shift) and AllowMove then begin
         OffsetRect(NewSize, dx, dy);
         if KeepInParent then Constrain_Move;
         DoMovingEvent;
      end;

      if (ssShift in Shift) and AllowSize then begin
         NewSize.Right  := NewSize.Right + dx;
         NewSize.Bottom := NewSize.Bottom + dy;
         if KeepInParent then Constrain_Size;
         DoSizingEvent;
      end;

      ResizeControl.BoundsRect := NewSize;
      ShowSizers;
   end;
end;

function TRaResizer.FindMoverByBuddy(c: TControl): TMover;
var
   i : integer;
begin
   Result := nil;
   for i := 0 to GroupMovers.Count-1 do
      if TMover(GroupMovers[i]).Buddy = c then
         Result := GroupMovers[i];
   Assert(Result <> nil);
end;

end.